import re
import tkinter as tk
import tkinter.ttk as ttk
from grid import Grid
import math
from tkinter import simpledialog, messagebox
from sapi import *
from drawingdialog import DrawingDialog, ChainDialog

import json
import time
import io
from tkinter.messagebox import showinfo

from PIL import Image, ImageTk, ImageOps

from collections import defaultdict

# from hcolor import *
# import subprocess
from color import *


class Board:
    def __init__(self):
        self.grid = Grid()
        self.cellSize = 48
        self.margin = 20
        self.boldLineSize = 3
        self.fineLineSize = 1
        self.valueFont = f"Arial {int(self.cellSize*0.8)}"  # tkDefaeultFont
        self.candFont = f"Arial {int(self.cellSize/3*0.8)}"

        root = tk.Tk()
        self.root = root
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()
        window_width = 800
        window_height = 500

        center_x = int(screen_width / 2 - window_width / 2)
        center_y = int(screen_height / 2 - window_height / 2)

        root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
        root.resizable(None, None)

        root.title("Sudoku client")

        canvas = tk.Canvas(root, bg="white")
        self.canvas = canvas
        canvas.place(
            x=10,
            y=20,
            width=self.cellSize * 9 + self.margin * 2,
            height=self.cellSize * 9 + self.margin * 2,
        )
        self.canvas = canvas

        canvas.bind("<Button-1>", self.setValue)
        canvas.bind("<Control-Button-1>", self.removeCand)

        self.baseCells = []
        for rowNum in range(9):
            row = []
            for colNum in range(9):
                c = BaseCell(self, rowNum, colNum)
                row.append(c)
            self.baseCells.append(row)

        self.gridStack = []
        self.hiddenToNormals = []
        self.lines = []

        self.cellColorIndex = 0

        self.tip_init()

    def run(self):
        self.layoutCells()
        border = Border(self)
        border.draw()

        self.layoutButtons()

        self.root.mainloop()

    def layoutButtons(self):
        frame = ttk.Frame(self.root)
        frame.place(x=450, y=20, width=340, height=480)
        frame.columnconfigure(0, weight=1)
        frame.columnconfigure(1, weight=1)
        frame.columnconfigure(2, weight=1)

        newButton = ttk.Button(frame, text="new", command=self.new)
        newButton.grid(column=0, row=0, sticky=tk.N, padx=1, pady=1)
        # newButton.pack(side=tk.TOP, anchor=tk.W)

        cueButton = ttk.Button(frame, text="Cue", command=self.cue)
        cueButton.grid(column=1, row=0, sticky=tk.N, padx=1, pady=1)
        # cueButton.pack(side=tk.TOP, anchor=tk.W)

        backButton = ttk.Button(frame, text="Back", command=self.back)
        backButton.grid(column=2, row=0, sticky=tk.N, padx=1, pady=1)

        imageButton = ttk.Button(frame, text="image", command=self.image)
        imageButton.grid(column=0, row=1, sticky=tk.N, padx=1, pady=1)
        # drawButton.pack(side=tk.TOP, anchor=tk.W)

        drawButton = ttk.Button(frame, text="draw", command=self.open_drawing_dialog)
        drawButton.grid(column=1, row=1, sticky=tk.N, padx=1, pady=1)
        # drawButton.pack(side=tk.LEFT, anchor=tk.W)
        chainButton = ttk.Button(frame, text="chain", command=self.open_chain_dialog)
        chainButton.grid(column=2, row=1, sticky=tk.N, padx=1, pady=1)

        checkButton = ttk.Button(
            frame, text="check", command=lambda: self.check(msgLabel)
        )
        checkButton.grid(column=0, row=2, sticky=tk.N, padx=1, pady=1)

        solveButton = ttk.Button(
            frame, text="Solve", command=lambda: self.solve(msgLabel)
        )
        solveButton.grid(column=1, row=2, sticky=tk.N, padx=1, pady=1)

        makeButton = ttk.Button(frame, text="Make", command=lambda: self.make(msgLabel))
        makeButton.grid(column=2, row=2, sticky=tk.N, padx=1, pady=1)

        # tipFrame = ttk.Frame(frame, height=20, borderwidth=0, padding=0)
        # tipFrame.pack(side=tk.TOP, anchor=tk.W, expand=True)
        tipButton = ttk.Button(frame, text="tip", command=lambda: self.tip(msgLabel))
        tipButton.grid(column=0, row=3, sticky=tk.N, padx=1, pady=1)
        # tipButton.pack(side=tk.LEFT, anchor=tk.W)

        self.selected_strategy = tk.StringVar()
        self.selected_strategy.set("Step")
        self.strategy_cb = ttk.Combobox(frame, textvariable=self.selected_strategy)
        self.strategy_cb["values"] = [
            "Step",
            "Strategy",
        ]
        """
        strategy_cb["values"] = [
            "Any",
            "NWing",
            "Rectangle Elimination",
            "X-Wing",
            "Swordfish",
            "Jellyfish",
            "Squirmbag",
            "Finned X-Wing",
            "Finned Swordfish",
            "Finned Jellyfish",
            "Death Blossom",
            "Algined Pairs Exclusion",
            "ALS-XY-Wing",
            "ALS-XY-Chain",
            "ALS-XZ",
            "X-Chain with Group",
            "X-Chain",
            "XY-Chain",
            "AIC",
            "AIC with Group",
            "AIC with ALS",
            "Triple Cell Forcing Chain",
            "Quad Cell Forcing Chain",
            "Triple House Forcing Chain",
            "Quad House Forcing Chain",
            "Chute Remote Pairs",
            "Naked Single",
            "Hidden Single Box",
            "Hidden Single Col",
            "Hidden Single Row",
            "Last One Box",
            "Last One Col",
            "Last One Row",
            "Medusa",
            "SK Loops",
            "Sue De Coq",
            "Turbot Fish",
            "Unique Rectangle",
            "W-Wing",
            "XY-Wing",
            "XYZ-Wing",
            "WXYZ-Wing",
            "VWXYZ-Wing",
        ]
        """
        # self.strategy_cb["state"] = "readonly"
        self.strategy_cb.grid(
            column=1, row=3, sticky=tk.N, padx=1, pady=1, columnspan=2
        )
        # strategy_cb.pack(side=tk.LEFT, anchor=tk.W)
        self.strategy_cb.bind("<<ComboboxSelected>>", self.strategy_changed)

        # bind the selected value changes
        # msgFrame = ttk.Frame(frame, height=200, borderwidth=0, padding=0)
        # msgFrame.grid(column=0, row=4, sticky=tk.NS, padx=1, pady=1, columnspan=3, rowspan=4)
        # msgLabel = tk.Label(
        #     frame, justify=tk.LEFT, anchor=tk.N, wraplength=340, font=("", 10)
        # )
        # msgLabel.grid(
        #     column=0, row=4, sticky=tk.N, padx=1, pady=1, columnspan=3, rowspan=4
        # )
        # msgLabel.pack()
        # msgLabel.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        msgLabel = tk.Label(
            frame,
            height=12,
            justify=tk.LEFT,
            anchor=tk.W,
            wraplength=340,
            font=("", 10),
        )
        msgLabel.grid(column=0, row=4, sticky=tk.W, padx=1, pady=1, columnspan=3)

        openText = tk.Text(frame, height=12)
        openText.grid(column=0, row=5, sticky=tk.S, padx=1, pady=1, columnspan=3)
        # openText.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        openButton = ttk.Button(
            frame, text="input", command=lambda: self.gridInput(openText)
        )
        openButton.grid(column=0, row=6, sticky=tk.S, padx=1, pady=1)
        # openButton.pack(side=tk.LEFT, anchor=tk.S)

        outButton = ttk.Button(
            frame, text="output", command=lambda: self.gridOutput(openText)
        )
        outButton.grid(column=1, row=6, sticky=tk.S, padx=1, pady=1)
        # outButton.pack(side=tk.LEFT, anchor=tk.S)

        fetchButton = ttk.Button(
            frame, text="fetch", command=lambda: self.gridFetch(openText)
        )
        fetchButton.grid(column=2, row=6, sticky=tk.S, padx=1, pady=1)
        # fetchButton.pack(side=tk.LEFT, anchor=tk.S)

    def open_chain_dialog(self):
        dialog = ChainDialog(self.root, "Draw chain and ...", AllCandColors)
        shape, description, color = dialog.result

        if shape == "chain":
            chain = description.split(",")
            if len(chain) > 1:
                chain, factor = chain[0], float(chain[1])
            else:
                chain, factor = chain[0], 1
            link = analyzeLink(chain)
            self.drawLink(link, factor)
        elif shape == "cand":
            for candDesc in description.split(","):
                candsPattern = r"R(\d+)C(\d+)\[(\d+)\]"
                cands = []
                fs = re.fullmatch(candsPattern, candDesc)
                if fs:
                    f = fs.group(1, 2, 3)
                    rows = [int(e) - 1 for e in f[0]]
                    cols = [int(e) - 1 for e in f[1]]
                    values = [int(e) for e in f[2]]
                    print(rows, cols, values, candDesc)
                    for row in rows:
                        for col in cols:
                            for value in values:
                                circle = self.baseCells[row][col].candCircles[value - 1]
                                # color = HelpCandsColors[3]
                                self.canvas.itemconfig(
                                    circle, fill=color, state="normal"
                                )
                                self.hiddenToNormals.append(circle)
        elif shape == "cell":
            color = HelpCellsColors[self.cellColorIndex]
            for cellDesc in description.split(","):
                cellDesc = cellDesc.strip()
                cellDesc = cellDesc.replace("'", "")
                cellDesc = cellDesc.replace('"', "")
                cells = self.analyzeCells(cellDesc)
                for cell in cells:
                    block = self.baseCells[cell.rownum][cell.colnum].block
                    self.canvas.itemconfig(block, fill=color, state="normal")
                    self.hiddenToNormals.append(block)
            self.cellColorIndex += 1

    def analyzeCells(self, cellDesc):
        cells = []
        pattern = r"R(\d)C(\d)"
        fs = re.fullmatch(pattern, cellDesc)
        if fs:
            f = fs.group(1, 2)
            cells.append(RCell(int(f[0]) - 1, int(f[1]) - 1))

            return cells

        pattern = r"R(\d)C(\d+)"
        fs = re.fullmatch(pattern, cellDesc)
        if fs:
            f = fs.group(1, 2)
            for colNum in sorted(f[1]):
                cells.append(RCell(int(f[0]) - 1, int(colNum) - 1))

            return cells

        pattern = r"R(\d+)C(\d)"
        fs = re.fullmatch(pattern, cellDesc)
        if fs:
            f = fs.group(1, 2)
            for rowNum in sorted(f[0]):
                cells.append(RCell(int(rowNum) - 1, int(f[1]) - 1))

            return cells

        pattern = r"B(\d)P(\d+)"
        fs = re.fullmatch(pattern, cellDesc)
        if fs:
            f = fs.group(1, 2)
            box = int(f[0])
            start_row = (box - 1) // 3 * 3
            start_col = (box - 1) % 3 * 3
            for pos in sorted(f[1]):
                row = start_row + (int(pos) - 1) // 3
                col = start_col + (int(pos) - 1) % 3
                cells.append(RCell(row, col))

            return cells

        return cells

    def open_drawing_dialog(self):
        dialog = DrawingDialog(self.root)
        if dialog.result:
            shape = dialog.result["shape"]
            row1, col1, cand1 = dialog.result["position1"]
            row1, col1, cand1 = int(row1) - 1, int(col1) - 1, int(cand1)
            row2, col2, cand2 = dialog.result["position2"]
            row2, col2, cand2 = int(row2) - 1, int(col2) - 1, int(cand2)
            x1, y1 = self.getCandCenter(row1, col1, cand1)
            x2, y2 = self.getCandCenter(row2, col2, cand2)
            if x1 > x2:
                x1 = x1 - self.cellSize // 6 + 2
                x2 = x2 + self.cellSize // 6 - 2
            elif x1 < x2:
                x1 = x1 + self.cellSize // 6 - 2
                x2 = x2 - self.cellSize // 6 + 2
            if y1 > y2:
                y1 = y1 - self.cellSize // 6 + 2
                y2 = y2 + self.cellSize // 6 - 2
            elif y1 < y2:
                y1 = y1 + self.cellSize // 6 - 2
                y2 = y2 - self.cellSize // 6 + 2
            linewidth = int(dialog.result["linewidth"])
            color = f'#{dialog.result["color"]}'
            linetype = dialog.result["linetype"]
            if linetype == "Solid":
                linetype = ()
            else:
                linetype = (5, 5)

            arrow = dialog.result["arrow"]
            if arrow:
                arrow = tk.LAST
            else:
                arrow = tk.NONE
            if shape == "Line":
                line = self.canvas.create_line(
                    x1,
                    y1,
                    x2,
                    y2,
                    smooth=True,
                    fill=color,
                    dash=linetype,
                    width=linewidth,
                    arrow=arrow,
                )
                self.lines.append(line)

    def strategy_changed(self, event):
        """handle the month changed event"""
        # showinfo(title="Result", message=self.selected_strategy.get())
        if self.selected_strategy.get() != "Strategy":
            return

        resp = rpc_strategy()
        print(resp)

        error = resp["error"]
        if error:
            # msgLabel.configure(text=error)
            return None

        # print("####", resp)
        result = resp["result"]
        result = json.loads(result)
        result.append("Strategy")
        print("result!!!:", type(result), result)

        self.selected_strategy.set("Step")
        self.strategy_cb["values"] = result
        # pass

    def layoutCells(self):
        for i, row in enumerate(self.grid.rows):
            for j, cell in enumerate(row):
                if cell.value != 0:
                    self.baseCells[i][j].setValue(cell.value, cell.given)
                else:
                    self.baseCells[i][j].setCands(cell.cands)

    def getPosition(self, x, y):
        x -= self.margin
        y -= self.margin
        rowNum = y // self.cellSize
        colNum = x // self.cellSize
        dx = x - self.cellSize * colNum
        dy = y - self.cellSize * rowNum
        cand = 1 + dy // (self.cellSize // 3) * 3 + dx // (self.cellSize // 3)
        return rowNum, colNum, cand

    def setValue(self, event):
        rowNum, colNum, cand = self.getPosition(event.x, event.y)
        c = self.baseCells[rowNum][colNum]
        if (
            self.grid.rows[rowNum][colNum].value != 0
            or cand not in self.grid.rows[rowNum][colNum].cands
        ):
            return
        self.gridStack.append(self.grid.copy())
        c.setValue(cand)
        cs = self.grid.solveOne(rowNum, colNum, cand)
        # clear cands
        for x in cs:
            self.baseCells[x.rowNum][x.colNum].setCands(x.cands)

        # print("setvalue", c.candCircles)
        self.turnNormalToHidden()
        self.tip_init()
        # for circle in c.candCircles:
        #            self.canvas.itemconfig(circle, state="hidden")

    def removeCand(self, event):
        rowNum, colNum, cand = self.getPosition(event.x, event.y)
        c = self.baseCells[rowNum][colNum]
        if (
            self.grid.rows[rowNum][colNum].value != 0
            or cand not in self.grid.rows[rowNum][colNum].cands
        ):
            return
        copy = self.grid.copy()
        if not self.grid.removeCand(rowNum, colNum, cand):
            return
        self.gridStack.append(copy)
        c.setCands(self.grid.rows[rowNum][colNum].cands)

        circle = c.candCircles[cand - 1]
        self.canvas.itemconfig(circle, state="hidden")
        self.tip_init()

    def new(self):
        self.turnNormalToHidden()
        self.gridStack.append(self.grid.copy())
        self.grid.toZero()
        self.grid.init()
        self.layoutCells()
        self.tip_init()

    def cue(self):
        self.turnNormalToHidden()
        self.gridStack.append(self.grid.copy())
        for row in self.grid.rows:
            for cell in row:
                if cell.value != 0:
                    cell.given = True
        self.layoutCells()

    def back(self):
        self.turnNormalToHidden()
        if len(self.gridStack) == 0:
            return
        self.grid = self.gridStack.pop()
        self.layoutCells()
        self.tip_init()

    """
    def convert_ps_to_png(self, ps_file, png_file):
        try:
            # Construct the Ghostscript command
            command = [
                "gs",  # Ghostscript command
                "-dSAFER",  # Enable safer mode
                "-dBATCH",  # Exit after processing
                "-dNOPAUSE",  # Do not pause for user input
                "-sDEVICE=png16m",  # Output device (16M colors PNG)
                "-r300",  # Resolution (300 DPI)
                "-g3000x3000",
                "-sOutputFile={}".format(png_file),  # Output file
                ps_file,  # Input PostScript file
                f"-sOutputFile={png_file}",  # Output file
                ps_file,  # Input PostScript file
            ]

            # Execute the command
            subprocess.run(command, check=True)
            print(f"Conversion successful: {png_file}")
        except subprocess.CalledProcessError as e:
            print(f"Error converting file: {e}")
    """

    def image(self):
        w = self.cellSize * 9 + self.margin * 2
        h = w

        data = self.canvas.postscript(
            # file="temp.ps",
            colormode="color",
            width=w,
            height=h,
            pagewidth=w * 10,
            pageheight=h * 10,
        )
        data_bytes = io.BytesIO(data.encode("utf-8"))
        img = Image.open(data_bytes)
        img = img.resize(
            (w * 2, h * 2),
        )
        # img = img.resize((w * 10, h * 10))
        t = time.strftime("%y%m%d%H%M%S", time.localtime())
        filename = f"sudoku{t}.png"
        img.save(
            filename,
            quality=95,
            dpi=(300, 300),
        )
        # print(w, h, self.canvas.winfo_width(), self.canvas.winfo_height())
        # print("ps saved")
        # t = time.strftime("%y%m%d%H%M%S", time.localtime())
        # filename = f"sudoku{t}.png"
        # self.convert_ps_to_png("temp.ps", filename)

    def check(self, msgLabel):
        resp = rpc_check(self.grid.encode())

        error = resp["error"]
        if error:
            msgLabel.configure(text=error)
            return None

        result = resp["result"]

        msgLabel.configure(text=result)

    def solve(self, msgLabel):
        resp = rpc_solve(self.grid.encode())

        error = resp["error"]
        if error:
            msgLabel.configure(text=error)
            return None

        result = resp["result"]

        self.turnNormalToHidden()
        self.gridStack.append(self.grid.copy())
        self.grid.decode(result)
        self.grid.init()
        self.grid.clearup()
        self.layoutCells()
        self.tip_init()

    def make(self, msgLabel):
        resp = rpc_make(self.grid.encode())

        error = resp["error"]
        if error:
            msgLabel.configure(text=error)
            return None

        results = json.loads(resp["result"])

        for result in results:
            print(result)

    def gridInput(self, openText):
        self.turnNormalToHidden()
        s = openText.get("1.0", "end")
        if len(s) == 0:
            return
        self.grid.decode(s)
        self.grid.init()
        self.grid.clearup()
        self.layoutCells()
        self.tip_init()

    def gridFetch(self, openText):
        openText.delete(1.0, tk.END)
        openText.insert(tk.END, self.grid.formatEncode())

    def gridOutput(self, openText):
        openText.delete(1.0, tk.END)
        openText.insert(tk.END, self.grid.output())

    def turnNormalToHidden(self):
        for x in self.hiddenToNormals:
            self.canvas.itemconfig(x, state="hidden")
        self.hiddenToNormals = []
        for line in self.lines:
            self.canvas.delete(line)
        self.lines = []
        self.cellColorIndex = 0

    def tip_init(self):
        self.tip_num = 0
        self.tip_results = []

    def tip(self, msgLabel):
        if self.tip_num < len(self.tip_results):
            print("tip_num", self.tip_num, self.tip_results)
            result = self.tip_results[self.tip_num]
            self.tip_num += 1
            self.tip_handle_result(result, msgLabel)
            return
        else:
            self.tip_init()
            resp = rpc_step(self.selected_strategy.get(), self.grid.encode())
            error = resp["error"]
            if error:
                msgLabel.configure(text=error)
                return None

            # print("####", resp)c
            print(resp["result"])
            self.tip_results = json.loads(resp["result"])
            self.tip_num = 0
            result = self.tip_results[self.tip_num]
            self.tip_num += 1
            self.tip_handle_result(result, msgLabel)

    def tip_handle_result(self, result, msgLabel):
        self.turnNormalToHidden()

        result = json.loads(result)

        s = ""
        for k, v in result.items():
            if v:
                s += f"{k}:{v}\n"
        msgLabel.configure(text=s)

        self.tip_result(result)

    def tip_result(self, result):
        rresult = unmarshal(result)
        # print(resp)
        if not rresult:
            return

        blockColors = defaultdict(list)

        for i, hs in enumerate(rresult.helphouses):
            for h in hs:
                if h.kind == "R":
                    cs = self.grid.rows[h.num]
                elif h.kind == "C":
                    cs = self.grid.cols[h.num]
                else:
                    cs = self.grid.boxes[h.num]
                # print("cs:", cs)
                for c in cs:
                    bc = self.baseCells[c.rowNum][c.colNum]
                    color = HelpCellsColors[i]
                    blockColors[bc.block].append(color)
                    # color = HelpCellsColors[i]
                    # self.canvas.itemconfig(bc.block, fill=color, state="normal")
                    # self.hiddenToNormals.append(bc.block)

        for i, hc in enumerate(rresult.helpcells):
            for c in hc:
                bc = self.baseCells[c.rownum][c.colnum]
                color = HelpCellsColors[i]
                blockColors[bc.block].append(color)
                #
                # self.canvas.itemconfig(bc.block, fill=color, state="normal")
                # self.hiddenToNormals.append(bc.block)

        for b, cs in blockColors.items():
            color = cs[0]
            for c in cs[1:]:
                color = blend_colors(color, c, 0.5)
            self.canvas.itemconfig(b, fill=color, state="normal")
            self.hiddenToNormals.append(b)

        for i, cs in enumerate(rresult.helpcands):
            for c in cs:
                for value in c.cands:
                    circle = self.baseCells[c.rownum][c.colnum].candCircles[value - 1]
                    color = HelpCandsColors[i]
                    self.canvas.itemconfig(circle, fill=color, state="normal")
                    self.hiddenToNormals.append(circle)

        # print("rresult.helpvalues", rresult.helpvalues)
        for i, hv in enumerate(rresult.helpvalues):
            print("helpvalues", hv)
            for v in hv:
                # print("helpvalues", v)
                circle = self.baseCells[v.rownum][v.colnum].valueCircle
                color = HelpCandsColors[i]
                self.canvas.itemconfig(circle, fill=color, state="normal")
                self.hiddenToNormals.append(circle)

        if rresult.chosen:
            c = rresult.chosen
            # print("choose", c)
            circle = self.baseCells[c.rownum][c.colnum].candCircles[c.value - 1]
            self.canvas.itemconfig(circle, fill=chosenColor, state="normal")
            self.hiddenToNormals.append(circle)

            block = self.baseCells[c.rownum][c.colnum].block
            if rresult.strategyname == "Guess":
                self.canvas.itemconfig(block, fill=guessBg, state="normal")
            else:
                self.canvas.itemconfig(block, fill=chosenBg, state="normal")
            self.hiddenToNormals.append(block)

        for i, link in enumerate(rresult.helplinks):
            self.drawLink(link)

        for c in rresult.removed:
            # print("remove", c)
            for value in c.cands:
                circle = self.baseCells[c.rownum][c.colnum].candCircles[value - 1]
                self.canvas.itemconfig(circle, fill=removeColor, state="normal")
                self.hiddenToNormals.append(circle)

    def drawLinkCands(self, link):
        headCircles = set()
        for cell in link.rnodes[0].rcells:
            if not cell:
                break
            circle = self.baseCells[cell.rownum][cell.colnum].candCircles[
                link.rnodes[0].cand - 1
            ]
            headCircles.add(circle)

        if link.chains[0] == "=":
            color = linkRedColor
        else:
            color = linkGreenColor
        for node in link.rnodes[:-1]:
            for cell in node.rcells:
                if not cell:
                    break
                circle = self.baseCells[cell.rownum][cell.colnum].candCircles[
                    node.cand - 1
                ]
                self.canvas.itemconfig(circle, fill=color, state="normal")
                self.hiddenToNormals.append(circle)
            if color == linkRedColor:
                color = linkGreenColor
            else:
                color = linkRedColor
        for cell in link.rnodes[-1].rcells:
            if not cell:
                break
            bc = self.baseCells[cell.rownum][cell.colnum]
            circle = bc.candCircles[link.rnodes[-1].cand - 1]
            if circle in headCircles:
                i = link.rnodes[-1].cand - 1
                c = self.canvas.create_oval(
                    bc.x0 + i % 3 * self.cellSize // 3 + 1,
                    bc.y0 + i // 3 * self.cellSize // 3 + 1,
                    bc.x0 + (i % 3 + 1) * self.cellSize // 3 - 1,
                    bc.y0 + (i // 3 + 1) * self.cellSize // 3 - 1,
                    fill="",
                    outline=color,
                    width=4,
                    # state="hidden",
                )
                self.lines.append(c)
            else:
                self.canvas.itemconfig(circle, fill=color, state="normal")
                self.hiddenToNormals.append(circle)

            # print("drawLinkCands", cell)

    def calculate_control_points(self, x1, y1, x2, y2, factor=1):
        mid_x = (x1 + x2) / 2
        mid_y = (y1 + y2) / 2

        dx = x2 - x1
        dy = y2 - y1

        print(x1, y1, x2, y2, dx, dy)

        length = math.sqrt(dx**2 + dy**2)
        # rate = 1 - 0.2 * ((length / self.cellSize) ** 0.2)
        ul = length / self.cellSize * 3
        if ul < 1.5:
            rate = 1.5
        elif ul < 4.5:
            rate = 0.4
        elif ul < 7.5:
            rate = 0.3
        else:
            rate = 0.2
        # print(ul, rate)

        rate = -rate * factor

        offset = length * rate

        perp_dx = -dy / length
        perp_dy = dx / length

        # cx = mid_x - perp_dx * offset
        # cy = mid_y - perp_dy * offset
        cx = mid_x + perp_dx * offset
        cy = mid_y + perp_dy * offset

        return cx, cy

    def drawLink(self, link, factor=1):
        self.drawLinkCands(link)
        for node in link.rnodes:
            self.drawNodeFrame(node)
        for i in range(len(link.rnodes) - 1):
            node1 = link.rnodes[i]
            node2 = link.rnodes[i + 1]
            self.drawNodeFrame(node1)
            x1, y1 = self.getNodeCenter(node1)
            x2, y2 = self.getNodeCenter(node2)
            # dx = x2 - x1
            # dy = y2 - y1
            # curve_offset = self.cellSize // 4 + int(math.sqrt(dx * dx + dy * dy) * 0.1)
            # mid_x = x1 + dx / 2 + (curve_offset if i % 2 == 0 else -curve_offset)
            # mid_y = y1 + dy / 2 + (curve_offset if i % 2 == 0 else -curve_offset)
            mid_x, mid_y = self.calculate_control_points(x1, y1, x2, y2, factor)
            if mid_x < 0:
                mid_x = 0
            if mid_x > self.canvas.winfo_width():
                mid_x = self.canvas.winfo_width()
            if mid_y < 0:
                mid_y = 0
            if mid_y > self.canvas.winfo_height():
                mid_y = self.canvas.winfo_height()

            if link.chains[i] == "=":
                color = strongColor
                line_width = 3
                dash = None
            else:
                color = weakColor
                line_width = 2
                dash = (5, 2)

            line = self.canvas.create_line(
                x1,
                y1,
                mid_x,
                mid_y,
                x2,
                y2,
                smooth=True,
                fill=color,
                width=line_width,
                arrow=tk.LAST,
                dash=dash,
            )
            self.lines.append(line)

            for cell in node1.rcells:
                r, c, cand = cell.rownum, cell.colnum, node1.cand
                digit = self.baseCells[r][c].textCands[cand - 1]
                self.canvas.tag_raise(digit)
            for cell in node2.rcells:
                r, c, cand = cell.rownum, cell.colnum, node2.cand
                digit = self.baseCells[r][c].textCands[cand - 1]
                self.canvas.tag_raise(digit)

    def drawNodeFrame(self, node):
        if node.kind == "rowgroup" or node.kind == "colgroup":
            cell1 = node.rcells[0]
            cell2 = node.rcells[-1]
            x1, y1 = self.getCandCenter(cell1.rownum, cell1.colnum, node.cand)
            d = self.cellSize // 6 - 1
            x1 -= d
            y1 -= d
            x2, y2 = self.getCandCenter(cell2.rownum, cell2.colnum, node.cand)
            x2 += d
            y2 += d
            print(x1, y1, x2, y2)
            rect = self.canvas.create_rectangle(
                (x1, y1),
                (x2, y2),
                outline=groupColor,
                width=2,
            )
            self.lines.append(rect)
        elif node.kind == "boxgroup":
            if len(node.rcells) != 2:
                return
            cell1 = node.rcells[0]
            cell2 = node.rcells[-1]
            d = self.cellSize // 6 - 1
            x1, y1 = self.getCandCenter(cell1.rownum, cell1.colnum, node.cand)
            x2, y2 = self.getCandCenter(cell2.rownum, cell2.colnum, node.cand)
            points = []
            if x1 < x2:
                points.append((x1 - d, y1 + d))
                points.append((x1 - d, y1 - d))
                points.append((x1 + d, y1 - d))
                points.append((x2 + d, y2 - d))
                points.append((x2 + d, y2 + d))
                points.append((x2 - d, y2 + d))
            else:
                points.append((x1 - d, y1 - d))
                points.append((x1 + d, y1 - d))
                points.append((x1 + d, y1 + d))
                points.append((x2 + d, y2 + d))
                points.append((x2 - d, y2 + d))
                points.append((x2 - d, y2 - d))
            poly = self.canvas.create_polygon(
                points,
                fill="",
                outline=groupColor,
                width=2,
            )
            self.lines.append(poly)

    def getCandCenter(self, row, col, cand):
        x = (
            self.cellSize * col
            + self.margin
            + (cand - 1) % 3 * self.cellSize // 3
            + self.cellSize // 6
        )
        y = (
            self.cellSize * row
            + self.margin
            + (cand - 1) // 3 * self.cellSize // 3
            + self.cellSize // 6
        )
        return x, y

    def getNodeCenter(self, node):
        x, y = 0, 0
        cand = node.cand
        for cell in node.rcells:
            row = cell.rownum
            col = cell.colnum
            cx, cy = self.getCandCenter(row, col, cand)
            x += cx
            y += cy
        n = len(node.rcells)
        return x // n, y // n


class Border:
    def __init__(self, board):
        self.board = board
        self.canvas = board.canvas

    def draw(self):
        # Draw the main grid
        self.canvas.create_rectangle(
            self.board.margin,
            self.board.margin,
            self.board.cellSize * 9 + self.board.margin,
            self.board.cellSize * 9 + self.board.margin,
            width=self.board.boldLineSize,
            outline="black",
        )

        # Draw horizontal and vertical lines
        left = self.board.margin
        right = left + self.board.cellSize * 9
        top = self.board.margin + self.board.cellSize
        bottom = top
        for i in range(8):
            if i in (2, 5):
                width = self.board.boldLineSize
                color = "black"
            else:
                width = self.board.fineLineSize
                color = "gray"
            self.canvas.create_line(
                left,
                top,
                right,
                bottom,
                width=width,
                fill=color,
            )
            self.canvas.create_line(
                top,
                left,
                bottom,
                right,
                width=width,
                fill=color,
            )
            top += self.board.cellSize
            bottom = top

        # Add row numbers (1-9) on the left side
        font_size = int(self.board.cellSize * 0.2)
        row_font = f"Arial {font_size}"
        for i in range(9):
            y = self.board.margin + i * self.board.cellSize + self.board.cellSize // 2
            self.canvas.create_text(
                self.board.margin // 2, y, text=str(i + 1), font=row_font, fill="black"
            )

        # Add column numbers (1-9) on the top
        for i in range(9):
            x = self.board.margin + i * self.board.cellSize + self.board.cellSize // 2
            self.canvas.create_text(
                x, self.board.margin // 2, text=str(i + 1), font=row_font, fill="black"
            )


class BaseCell:
    def __init__(self, board, rowNum, colNum):
        self.board = board
        self.canvas = board.canvas
        # self.cell = board.grid.rows[rowNum][colNum]
        self.x0 = colNum * self.board.cellSize + self.board.margin
        self.y0 = rowNum * self.board.cellSize + self.board.margin

        self.generate()

    def generateBlock(self):
        self.block = self.canvas.create_rectangle(
            self.x0,
            self.y0,
            self.x0 + self.board.cellSize,
            self.y0 + self.board.cellSize,
            fill="white",
            outline="",
            state="hidden",
        )

    def generateValueCircle(self):
        self.valueCircle = self.canvas.create_oval(
            self.x0 + int(self.board.cellSize * 0.1),
            self.y0 + int(self.board.cellSize * 0.1),
            self.x0 + self.board.cellSize - int(self.board.cellSize * 0.1),
            self.y0 + self.board.cellSize - int(self.board.cellSize * 0.1),
            fill="white",
            outline="",
            state="hidden",
        )

    def generateCandCircles(self):
        self.candCircles = []
        for i in range(9):
            c = self.canvas.create_oval(
                self.x0 + i % 3 * self.board.cellSize // 3 + 1,
                self.y0 + i // 3 * self.board.cellSize // 3 + 1,
                self.x0 + (i % 3 + 1) * self.board.cellSize // 3 - 1,
                self.y0 + (i // 3 + 1) * self.board.cellSize // 3 - 1,
                fill="white",
                outline="",
                state="hidden",
            )
            self.candCircles.append(c)

    def generateValue(self):
        x = self.x0 + self.board.cellSize // 2
        y = self.y0 + self.board.cellSize // 2
        self.textValue = self.canvas.create_text(
            (x, y),
            text="0",
            fill="black",
            font=self.board.valueFont,
            state="hidden",
        )

    def generateCands(self):
        self.textCands = []
        for i in range(9):
            x = self.x0 + i % 3 * self.board.cellSize // 3 + self.board.cellSize // 6
            y = self.y0 + i // 3 * self.board.cellSize // 3 + self.board.cellSize // 6
            c = self.canvas.create_text(
                (x, y),
                text=str(i + 1),
                font=self.board.candFont,
                fill="black",
                state="hidden",
            )
            self.textCands.append(c)

    def generate(self):
        self.generateBlock()
        # self.board.canvas.itemconfig(self.block, fill="orange", state="normal")
        self.generateValueCircle()
        self.generateCandCircles()
        self.generateValue()
        self.generateCands()

    def setValue(self, value, given=False):
        for c in self.textCands:
            self.canvas.itemconfig(c, state="hidden")
        if given:
            color = givenColor
        else:
            color = "black"
        self.canvas.itemconfig(
            self.textValue, text=str(value), fill=color, state="normal"
        )

    def setCands(self, cands):
        self.canvas.itemconfig(self.textValue, state="hidden")
        for i, c in enumerate(self.textCands, start=1):
            if i in cands:
                self.canvas.itemconfig(c, state="normal")
            else:
                self.canvas.itemconfig(c, state="hidden")


if __name__ == "__main__":
    s = """
	032 000 000
	867 000 019
	095 000 082

	000 702 091
	719 683 245
	000 109 003

	050 000 920
	970 000 034
	000 000 150
    """
    app = Board()
    app.grid.decode(s)
    app.grid.init()
    app.grid.clearup()

    app.run()
